/* MediaUtil LLJTran - $RCSfile: Exif.java,v $
 * Copyright (C) 1999-2005 Dmitriy Rogatkin, Suresh Mahalingam.  All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
 *  ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 *  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *  $Id: Exif.java,v 1.6 2006/05/13 07:11:29 msuresh Exp $
 *
 * Some ideas and algorithms were borrowed from:
 * Thomas G. Lane, and James R. Weeks
 */
package mediautil.image.jpeg;

/**
 * For building this class were used the following sources
 * 1. Thierry Bousch <bousch@topo.math.u-psud.fr>
 * 2. ISO/DIS 12234-2
 *    Photography - Electronic still picture cameras - Removable Memory
 *    Part 2: Image data format - TIFF/EP (http://www.pima.net/it10a.htm)
 * 3. <a href="http://www.pima.net/standards/it10/PIMA15740/exif.htm"> some enhancements were based on </a>
 */

import mediautil.gen.FileFormatException;
import mediautil.gen.Log;
import mediautil.gen.Rational;
import mediautil.image.ImageResources;

import javax.swing.*;
import java.awt.*;
import java.awt.image.MemoryImageSource;
import java.io.*;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;

// TODO: add loading custom/manufac exif specific properties from
// database in form of XML file
// where primary key is combination make+model

/**
 * This class represent the Exif header providing additional information about
 * image. It is organized in a similiar IFD directory structure as specified by
 * the Exif spec.<p>
 *
 * The below shows the most common usage of creating and changing an Exif
 * Entry:<p>
 *
 *<pre>
 *      // Use appropriate code to read an image into llj
 *      LLJTran llj = new LLJTran(..); llj.read(..);
 *      Exif exif = (Exif) llj.getImageInfo();
 *
 *      // Create and entry for the EXIFIMAGEWIDTH tag
 *      // LONG is the appropriate Data Type for the EXIFIMAGEWIDTH tag
 *      Entry e = new Entry(LONG);
 *      e.setValue(0, new Integer(llj.getWidth()));
 *      exif.setTagValue(Exif.EXIFIMAGEWIDTH, 0, e, true);
 *
 *      // Change the value of the DATETIME Entry
 *      Entry entry = exif.getTagValue(Exif.DATETIME, true);
 *      if(entry != null)
 *          entry.setValue(0, "1998:08:18 11:15:00");
 *</pre>
 *
 * Also go through the below sources whose help was used to build this class:<p>
 *
 * <ul>
 * <li>ISO/DIS 12234-2
 *    Photography - Electronic still picture cameras - Removable Memory
 *    Part 2: Image data format - TIFF/EP (http://www.pima.net/it10a.htm)
 * <li><a href="http://www.pima.net/standards/it10/PIMA15740/exif.htm"> some enhancements were based on </a>
 * </ul>
 *
 * @see #setTagValue(int,int, Entry,boolean)
 * @see Entry#setValue(int,Object)
 *
 * @author dmitriy
 *
 */
public class Exif extends AbstractImageInfo<LLJTran> {
    public final static String FORMAT = "Exif";

    public static final byte[] EXIF_MARK = { 0x45, 0x78, 0x69, 0x66, 0, 0 };

    static final int FIRST_IFD_OFF = 6;

    static final int MIN_JPEG_SIZE = 100;

    // Exif directory tag definition
    /** Identifies NEWSUBFILETYPE tag */
    public final static int NEWSUBFILETYPE = 0xFE;

    /** Identifies the IMAGEWIDTH tag */
    public final static int IMAGEWIDTH = 0x100;

    /** Identifies the IMAGELENGTH tag */
    public final static int IMAGELENGTH = 0x101;

    /** Identifies the BITSPERSAMPLE tag */
    public final static int BITSPERSAMPLE = 0x102;

    /** Identifies the COMPRESSION tag */
    public final static int COMPRESSION = 0x103;

    /** Identifies the PHOTOMETRICINTERPRETATION tag */
    public final static int PHOTOMETRICINTERPRETATION = 0x106;

    /** Identifies the FILLORDER tag */
    public final static int FILLORDER = 0x10A;

    /** Identifies the DOCUMENTNAME tag */
    public final static int DOCUMENTNAME = 0x10D;

    /** Identifies the IMAGEDESCRIPTION tag */
    public final static int IMAGEDESCRIPTION = 0x10E;

    /** Identifies the MAKE tag */
    public final static int MAKE = 0x10F;

    /** Identifies the MODEL tag */
    public final static int MODEL = 0x110;

    /** Identifies the STRIPOFFSETS tag */
    public final static int STRIPOFFSETS = 0x111;

    /** Identifies the ORIENTATION tag */
    public final static int ORIENTATION = 0x112;

    /** Identifies the SAMPLESPERPIXEL tag */
    public final static int SAMPLESPERPIXEL = 0x115;

    /** Identifies the ROWSPERSTRIP tag */
    public final static int ROWSPERSTRIP = 0x116;

    /** Identifies the STRIPBYTECOUNTS tag */
    public final static int STRIPBYTECOUNTS = 0x117;

    /** Identifies the XRESOLUTION tag */
    public final static int XRESOLUTION = 0x11A;

    /** Identifies the YRESOLUTION tag */
    public final static int YRESOLUTION = 0x11B;

    /** Identifies the PLANARCONFIGURATION tag */
    public final static int PLANARCONFIGURATION = 0x11C;

    /** Identifies the RESOLUTIONUNIT tag */
    public final static int RESOLUTIONUNIT = 0x128;

    /** Identifies the TRANSFERFUNCTION tag */
    public final static int TRANSFERFUNCTION = 0x12D;

    /** Identifies the SOFTWARE tag */
    public final static int SOFTWARE = 0x131;

    /** Identifies the DATETIME tag */
    public final static int DATETIME = 0x132;

    /** Identifies the ARTIST tag */
    public final static int ARTIST = 0x13B;

    /** Identifies the WHITEPOINT tag */
    public final static int WHITEPOINT = 0x13E;

    /** Identifies the PRIMARYCHROMATICITIES tag */
    public final static int PRIMARYCHROMATICITIES = 0x13F;

    /** Identifies the SUBIFDS tag */
    public final static int SUBIFDS = 0x14A;

    /** Identifies the JPEGTABLES tag */
    public final static int JPEGTABLES = 0x15B;

    /** Identifies the TRANSFERRANGE tag */
    public final static int TRANSFERRANGE = 0x156;

    /** Identifies the JPEGPROC tag */
    public final static int JPEGPROC = 0x200;

    /** Identifies the JPEGINTERCHANGEFORMAT tag */
    public final static int JPEGINTERCHANGEFORMAT = 0x201;

    /** Identifies the JPEGINTERCHANGEFORMATLENGTH tag */
    public final static int JPEGINTERCHANGEFORMATLENGTH = 0x202;

    /** Identifies the YCBCRCOEFFICIENTS tag */
    public final static int YCBCRCOEFFICIENTS = 0x211;

    /** Identifies the YCBCRSUBSAMPLING tag */
    public final static int YCBCRSUBSAMPLING = 0x212;

    /** Identifies the YCBCRPOSITIONING tag */
    public final static int YCBCRPOSITIONING = 0x213;

    /** Identifies the REFERENCEBLACKWHITE tag */
    public final static int REFERENCEBLACKWHITE = 0x214;

    /** Identifies the CFAREPEATPATTERNDIM tag */
    public final static int CFAREPEATPATTERNDIM = 0x828D;

    /** Identifies the CFAPATTERN tag */
    public final static int CFAPATTERN = 0x828E;

    /** Identifies the BATTERYLEVEL tag */
    public final static int BATTERYLEVEL = 0x828F;

    /** Identifies the COPYRIGHT tag */
    public final static int COPYRIGHT = 0x8298;

    /** Identifies the EXPOSURETIME tag */
    public final static int EXPOSURETIME = 0x829A;

    /** Identifies the FNUMBER tag */
    public final static int FNUMBER = 0x829D;

    /** Identifies the IPTC_NAA tag */
    public final static int IPTC_NAA = 0x83BB;

    /** Identifies the EXIFOFFSET tag */
    public final static int EXIFOFFSET = 0x8769;

    /** Identifies the ERCOLORPROFILE tag */
    public final static int INTERCOLORPROFILE = 0x8773;

    /** Identifies the EXPOSUREPROGRAM tag */
    public final static int EXPOSUREPROGRAM = 0x8822;

    /** Identifies the SPECTRALSENSITIVITY tag */
    public final static int SPECTRALSENSITIVITY = 0x8824;

    /** Identifies the GPSINFO tag */
    public final static int GPSINFO = 0x8825;

    /** Identifies the ISOSPEEDRATINGS tag */
    public final static int ISOSPEEDRATINGS = 0x8827;

    /** Identifies the OECF tag */
    public final static int OECF = 0x8828;

    /** Identifies the EXIFVERSION tag */
    public final static int EXIFVERSION = 0x9000;

    /** Identifies the DATETIMEORIGINAL tag */
    public final static int DATETIMEORIGINAL = 0x9003;

    /** Identifies the DATETIMEDIGITIZED tag */
    public final static int DATETIMEDIGITIZED = 0x9004;

    /** Identifies the COMPONENTSCONFIGURATION tag */
    public final static int COMPONENTSCONFIGURATION = 0x9101;

    /** Identifies the COMPRESSEDBITSPERPIXEL tag */
    public final static int COMPRESSEDBITSPERPIXEL = 0x9102;

    /** Identifies the SHUTTERSPEEDVALUE tag */
    public final static int SHUTTERSPEEDVALUE = 0x9201;

    /** Identifies the APERTUREVALUE tag */
    public final static int APERTUREVALUE = 0x9202;

    /** Identifies the BRIGHTNESSVALUE tag */
    public final static int BRIGHTNESSVALUE = 0x9203;

    /** Identifies the EXPOSUREBIASVALUE tag */
    public final static int EXPOSUREBIASVALUE = 0x9204;

    /** Identifies the MAXAPERTUREVALUE tag */
    public final static int MAXAPERTUREVALUE = 0x9205;

    /** Identifies the SUBJECTDISTANCE tag */
    public final static int SUBJECTDISTANCE = 0x9206;

    /** Identifies the METERINGMODE tag */
    public final static int METERINGMODE = 0x9207;

    /** Identifies the LIGHTSOURCE tag */
    public final static int LIGHTSOURCE = 0x9208;

    /** Identifies the FLASH tag */
    public final static int FLASH = 0x9209;

    /** Identifies the FOCALLENGTH tag */
    public final static int FOCALLENGTH = 0x920A;

    /** Identifies the MAKERNOTE tag */
    public final static int MAKERNOTE = 0x927C;

    /** Identifies the USERCOMMENT tag */
    public final static int USERCOMMENT = 0x9286;

    /** Identifies the SUBSECTIME tag */
    public final static int SUBSECTIME = 0x9290;

    /** Identifies the SUBSECTIMEORIGINAL tag */
    public final static int SUBSECTIMEORIGINAL = 0x9291;

    /** Identifies the SUBSECTIMEDIGITIZED tag */
    public final static int SUBSECTIMEDIGITIZED = 0x9292;

    /** Identifies the FLASHPIXVERSION tag */
    public final static int FLASHPIXVERSION = 0xA000;

    /** Identifies the COLORSPACE tag */
    public final static int COLORSPACE = 0xA001;

    /** Identifies the EXIFIMAGEWIDTH tag */
    public final static int EXIFIMAGEWIDTH = 0xA002;

    /** Identifies the EXIFIMAGELENGTH tag */
    public final static int EXIFIMAGELENGTH = 0xA003;

    /** Identifies the EROPERABILITYOFFSET tag */
    public final static int INTEROPERABILITYOFFSET = 0xA005;

    /** Identifies the FLASHENERGY tag */
    public final static int FLASHENERGY = 0xA20B; //  = 0x920B in TIFF/EP

    /** Identifies the SPATIALFREQUENCYRESPONSE tag */
    public final static int SPATIALFREQUENCYRESPONSE = 0xA20C; //  = 0x920C    -  -

    /** Identifies the FOCALPLANEXRESOLUTION tag */
    public final static int FOCALPLANEXRESOLUTION = 0xA20E; //  = 0x920E    -  -

    /** Identifies the FOCALPLANEYRESOLUTION tag */
    public final static int FOCALPLANEYRESOLUTION = 0xA20F; //  = 0x920F    -  -

    /** Identifies the FOCALPLANERESOLUTIONUNIT tag */
    public final static int FOCALPLANERESOLUTIONUNIT = 0xA210; //  = 0x9210    -  -

    /** Identifies the SUBJECTLOCATION tag */
    public final static int SUBJECTLOCATION = 0xA214; //  = 0x9214    -  -

    /** Identifies the EXPOSUREINDEX tag */
    public final static int EXPOSUREINDEX = 0xA215; //  = 0x9215    -  -

    /** Identifies the SENSINGMETHOD tag */
    public final static int SENSINGMETHOD = 0xA217; //  = 0x9217    -  -

    /** Identifies the FILESOURCE tag */
    public final static int FILESOURCE = 0xA300;

    /** Identifies the SCENETYPE tag */
    public final static int SCENETYPE = 0xA301;

    /** Identifies the FOCALLENGTHIN35MMFILM tag */
    public final static int FOCALLENGTHIN35MMFILM = 0xA405;

    /** Identifies the SHARPNESS tag */
    public final static int SHARPNESS = 0xA40A;

    /** Identifies the CUSTOMRENDERED tag */
    public final static int CUSTOMRENDERED = 0xA401;

    /** Identifies the EXPOSUREMODE tag */
    public final static int EXPOSUREMODE = 0xA402;

    /** Identifies the WHITEBALANCE tag */
    public final static int WHITEBALANCE = 0xA403;

    /** Identifies the DIGITALZOOMRATIO tag */
    public final static int DIGITALZOOMRATIO = 0xA404;

    /** Identifies the SATURATION tag */
    public final static int SATURATION = 0xA409;

    /** Identifies the SCENECAPTURETYPE tag */
    public final static int SCENECAPTURETYPE = 0xA406;

    /** Identifies the GAINCONTROL tag */
    public final static int GAINCONTROL = 0xA407;

    /** Identifies the CONTRAST tag */
    public final static int CONTRAST = 0xA408;

    /** Identifies the PRINTMODE tag */
    public final static int PRINTMODE = 0xC4A5;

    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSVersionID = 0x0000;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSLatitudeRef = 0x0001;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSLatitude = 0x0002;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSLongitudeRef = 0x0003;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSLongitude = 0x0004;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSAltitudeRef = 0x0005;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSAltitude = 0x0006;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSTimeStamp = 0x0007;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSSatellites = 0x0008;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSStatus = 0x0009;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSMeasureMode = 0x000a;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDOP = 0x000b;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSSpeedRef = 0x000c;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSSpeed = 0x000d;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSTrackRef = 0x000e;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSTrack = 0x000f;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSImgDirectionRef = 0x0010;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSImgDirection = 0x0011;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSMapDatum = 0x0012;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestLatitudeRef = 0x0013;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestLatitude = 0x0014;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestLongitudeRef = 0x0015;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestLongitude = 0x0016;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestBearingRef = 0x0017;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestBearing = 0x0018;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestDistanceRef = 0x0019;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDestDistance = 0x001a;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSProcessingMethod = 0x001b;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSAreaInformation = 0x001c;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDateStamp = 0x001d;
    /** GPS tag. Make sure you access this under the IFD for GPSINFO */
    public final static int GPSDifferential = 0x001e;

    // Exif directory type of tag definition
    /** Identifies the Byte Data Type */
    public final static int BYTE = 1;

    /** Identifies the ASCII Data Type */
    public final static int ASCII = 2;

    /** Identifies the  SHORT Data Type */
    public final static int SHORT = 3;

    /** Identifies the LONG Data Type */
    public final static int LONG = 4;

    /** Identifies the RATIONAL Data Type */
    public final static int RATIONAL = 5;

    /** Identifies the Signed BYTE Data Type */
    public final static int SBYTE = 6;

    /** Identifies the UNDEFINED Data Type */
    public final static int UNDEFINED = 7;

    /** Identifies the Signed SHORT Data Type */
    public final static int SSHORT = 8;

    /** Identifies the Signed LONG Data Type */
    public final static int SLONG = 9;

    /** Identifies the Signed RATIONAL Data Type */
    public final static int SRATIONAL = 10;

    public final static int ORIENTATION_TOPLEFT = 1;

    public final static int ORIENTATION_TOPRIGHT = 2;

    public final static int ORIENTATION_BOTRIGHT = 3;

    public final static int ORIENTATION_BOTLEFT = 4;

    public final static int ORIENTATION_LEFTTOP = 5;

    public final static int ORIENTATION_RIGHTTOP = 6;

    public final static int ORIENTATION_RIGHTBOT = 7;

    public final static int ORIENTATION_LEFTBOT = 8;

    // TODO: read names from XML database on camera vendor
    public final static String[] EXPOSURE_PROGRAMS = { "P0", "P1", "Normal",
            "P3", "P5" };

    public final static String[] METERING_MODES = { "P0", "P1", "Normal", "P3",
            "PATTERN" };

    final static int DIR_ENTRY_SIZE = 12;

    public final static int[] TYPELENGTH = { 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 };

    // TODO: consider replacing String name to java.io.File file
    /**
     * Loads the ImageInfo using information supplied. Uses the
     * {@link #readInfo()} method through AbstractImageInfo's constructor.
     * @param is Image input. This is not used by Exif.
     * @param data Image Header Information Marker Data excluding the 4 jpeg
     * marker  bytes
     * @param offset Offset of marker within Image Input
     * @param name Name of the Image File
     * @param comments Image comments
     * @param format Image Object of type LLJTran
     */
    public Exif(InputStream is, byte[] data, int offset, String name,
                String comments, LLJTran format) throws FileFormatException {
        super(is, data, offset, name, comments, format);
        // a unusual problem is here
        // no own variables are initialized here
        // but super's constructor calls our method read, which is using
        // uninitialized local variables, so they are moved to parent
    }

    /**
     * Basic constructor
     */
    public Exif() {
        ifds = new IFD[2];
        intel = true;
        version = 2;
    }

    public String getFormat() {
        return FORMAT;
    }

    public static byte[] getMarkerData() {
        return new byte[] { (byte) 0xFF, (byte) 0xE1, 0, 40, (byte) 0x45,
                (byte) 0x78, (byte) 0x69, (byte) 0x66, (byte) 0x00,
                (byte) 0x00, (byte) 0x49, (byte) 0x49, (byte) 0x2A,
                (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x00,
                (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x0F,
                (byte) 0x01, (byte) 0x02, (byte) 0x00, (byte) 0x05,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 26, 0, 0, 0,
                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 'F',
                (byte) 'A', (byte) 'K', (byte) 'E', (byte) 0x00, 0 };
    }

    /**
     * Gets the Entry corresponding to an Exif tag.
     *
     * @param tag Exif tag
     * @param main true if it is in the main IFD, false if it is in the Sub IFD.
     * Most of the commonly used Exif tags are in the main IFD. The Thumbnail
     * related tags are in the Sub IFD.
     * @return Entry corresponding to the tag
     */
    public Entry getTagValue(int tag, boolean main) {
        return getTagValue(new Integer(tag), -1, main);
    }

    /**
     * Gets the Entry corresponding to an Exif tag.
     *
     * @param tag Exif tag
     * @param subTag Sub Tag if any or pass -1
     * @param main true if it is in the main IFD, false if it is in the Sub IFD.
     * Most of the commonly used Exif tags are in the main IFD. The Thumbnail
     * related tags are in the Sub IFD.
     * @return Entry corresponding to the tag
     */
    public Entry getTagValue(Integer tag, int subTag, boolean main) {

        return ifds[main ? 0 : 1] != null ? ifds[main ? 0 : 1].getEntry(tag,
                subTag) : null;
    }

    /**
     * Sets the Entry corresponding to an Exif tag.
     *
     * @param tag Exif tag
     * @param subTag Sub Tag if any or pass -1
     * @param value Entry to set
     * @param main true if it is in the main IFD, false if it is in the Sub IFD.
     * Most of the commonly used Exif tags are in the main IFD. The Thumbnail
     * related tags are in the Sub IFD.
     */
    public void setTagValue(int tag, int subTag, Entry value, boolean main) {
        if (ifds[main ? 0 : 1] != null)
            ifds[main ? 0 : 1].setEntry(new Integer(tag), subTag, value);
    }

    /**
     * Method to get the length of the Thumbnail.
     *
     * @return Length of the Thumbnail
     */
    public int getThumbnailLength() {
        int retVal = 0;

        Entry e = getTagValue(JPEGINTERCHANGEFORMATLENGTH, false);
        if (e == null)
            e = getTagValue(STRIPBYTECOUNTS, false);
        if (e != null)
            retVal = ((Integer) e.getValue(0)).intValue();
        return retVal;
    }

    /**
     * Method to get the offset of the Thumbnail within the imageInfo data.
     *
     * @return Offset of the Thumbnail within the Appx marker data
     */
    public int getThumbnailOffset() {
        int retVal = 0;

        Entry e = getTagValue(JPEGINTERCHANGEFORMAT, false);
        if (e == null)
            e = getTagValue(STRIPOFFSETS, false);
        if (e != null)
            retVal = ((Integer) e.getValue(0)).intValue() + FIRST_IFD_OFF;
        return retVal;
    }

    /** saves thumbnail image to specified path
     */
    public boolean saveThumbnailImage(OutputStream os) throws IOException {
        if (os == null || format == null)
            return false;
        boolean success = false;
        String ext = getThumbnailExtension();
        int offset = getThumbnailOffset();
        int length = getThumbnailLength();
        int min_len = ImageResources.EXT_JPEG.equals(ext) ? MIN_JPEG_SIZE : 0;
        if (offset > 0 && length > min_len) {
            InputStream is = null;
            try {
                if (ImageResources.EXT_JPEG.equals(ext)) {
                    is = new FileInputStream(getImageFile());
                    byte[] image = new byte[length];
                    skip(is, super.offset + offset);
                    read(is, image);
                    int jpeg_offset = 0;
                    while (!(image[jpeg_offset] == M_PRX && image[jpeg_offset + 1] == M_SOI)
                            && jpeg_offset < image.length - 1)
                        jpeg_offset++; // skip garbage in begining including padding FF
                    if (image.length - jpeg_offset > MIN_JPEG_SIZE) {
                        // if image can be consider as JPEG
                        os
                                .write(image, jpeg_offset, image.length
                                        - jpeg_offset);
                        success = true;
                    }
                } else if (ImageResources.EXT_BMP.equals(ext)) {
                    // save as BMP
                    // TODO: add BMP rotation
                    is = new FileInputStream(getImageFile());
                    skip(is, super.offset + offset);
                    Entry e = getTagValue(STRIPBYTECOUNTS, false);
                    if (e != null) {
                        length = ((Integer) e.getValue(0)).intValue();
                        int imgwidth = 0, imglength = 0;
                        e = getTagValue(IMAGEWIDTH, false);
                        if (e != null)
                            imgwidth = ((Integer) e.getValue(0)).intValue();
                        e = getTagValue(IMAGELENGTH, false);
                        if (e != null)
                            imglength = ((Integer) e.getValue(0)).intValue();
                        int bitspix = 8;
                        e = getTagValue(BITSPERSAMPLE, false);
                        if (e != null)
                            bitspix = ((Integer) e.getValue(0)).intValue();
                        int simpleperpix = 3;
                        e = getTagValue(SAMPLESPERPIXEL, false);
                        if (e != null)
                            simpleperpix = ((Integer) e.getValue(0)).intValue();
                        data = new byte[BMP24_HDR_SIZE];
                        System.arraycopy(BMP_SIG, 0, data, 0, BMP_SIG.length);
                        offset = 2;
                        int scanline_len = (imgwidth * simpleperpix + 3)
                                & (-1 << 2);
                        offset = i2bsI(offset, BMP24_HDR_SIZE + scanline_len
                                * imglength, 4);
                        offset = i2bsI(offset, 0, 4); // reserved
                        offset = i2bsI(offset, BMP24_HDR_SIZE, 4); // headersize (offset bits)
                        offset = i2bsI(offset, 0x28, 4); // infoSize
                        offset = i2bsI(offset, imgwidth, 4); // width
                        offset = i2bsI(offset, imglength, 4); // length
                        offset = i2bsI(offset, 1, 2); // biPlanes
                        offset = i2bsI(offset, simpleperpix * bitspix, 2); // bits
                        offset = i2bsI(offset, 0, 4); // biCompression
                        offset = i2bsI(offset, scanline_len * imglength, 4); // biSizeImage
                        offset = i2bsI(offset, 2834, 4); // biXPelsPerMeter
                        offset = i2bsI(offset, 2834, 4); // biYPelsPerMeter
                        offset = i2bsI(offset, 0, 4); // biClrUsed
                        offset = i2bsI(offset, 0, 4); // biClrImportant
                        os.write(data);
                        data = new byte[length];
                        read(is, data);
                        int filler = scanline_len - imgwidth * simpleperpix;
                        scanline_len = imgwidth * simpleperpix;
                        byte[] filldata = null;
                        if (filler != 0)
                            filldata = new byte[filler];
                        for (offset = length - scanline_len; offset >= 0; offset -= scanline_len) {
                            //os.write(data, offset, scanline_len);
                            for (int ro = 0; ro < scanline_len; ro += 3) {
                                os.write(data[offset + ro + 2]);
                                os.write(data[offset + ro + 1]);
                                os.write(data[offset + ro]);
                            }
                            if (filler != 0)
                                os.write(filldata);
                        }
                    }
                    success = true;
                }
            } finally {
                if (is != null)
                    is.close();
            }
        }
        if (success == false)
            return super.saveThumbnailImage(os);
        return true;
    }

    /**
     * Gets the extension of the Thumbnail Image format.
     * Returns null if the image has no Thumbnail.
     * @return Thumbnail Extension as defined in ImageResources
     * @see ImageResources
     */
    public String getThumbnailExtension() {
        return (getTagValue(JPEGINTERCHANGEFORMAT, false) != null) ? ImageResources.EXT_JPEG
                : ((getTagValue(STRIPOFFSETS, false) != null) ? ImageResources.EXT_BMP
                : null);
    }

    public Icon getThumbnailIcon(Dimension size) {
        String ext = getThumbnailExtension();
        int offset = getThumbnailOffset();
        int length = getThumbnailLength();
        int min_len = ImageResources.EXT_JPEG.equals(ext) ? MIN_JPEG_SIZE : 0;
        if (offset > 0 && length > min_len) {
            InputStream is = null;
            try {
                if (ImageResources.EXT_JPEG.equals(ext)) {
                    int jpeg_offset = 0;
                    try {
                        is = new FileInputStream(getImageFile());
                        byte[] image = new byte[length];
                        skip(is, super.offset + offset);
                        is.read(image);
                        while (!(image[jpeg_offset] == M_PRX && image[jpeg_offset + 1] == M_SOI)
                                && jpeg_offset < image.length - 1)
                            jpeg_offset++; // skip garbage in begining including padding FF
                        if (jpeg_offset < image.length - MIN_JPEG_SIZE)
                            return new ImageIcon(Toolkit.getDefaultToolkit()
                                    .createImage(image, jpeg_offset,
                                            image.length - jpeg_offset));
                    } catch (IOException e) {
                    } catch (ArrayIndexOutOfBoundsException e) {
                        if(Log.debugLevel >= Log.LEVEL_ERROR)
                            System.err.println("Bad index " + jpeg_offset + " for "
                                    + getName());
                    }
                } else if (ImageResources.EXT_BMP.equals(ext)) {
                    try {
                        is = new FileInputStream(getImageFile());
                        skip(is, super.offset + offset);
                        Entry e = getTagValue(STRIPBYTECOUNTS, false);
                        if (e != null) {
                            length = ((Integer) e.getValue(0)).intValue();
                            data = new byte[length];
                            read(is, data);
                            int imgwidth = 0, imglength = 0;

                            e = getTagValue(IMAGEWIDTH, false);
                            if (e != null)
                                imgwidth = ((Integer) e.getValue(0)).intValue();
                            e = getTagValue(IMAGELENGTH, false);
                            if (e != null)
                                imglength = ((Integer) e.getValue(0))
                                        .intValue();
                            int bitspix = 8;
                            e = getTagValue(BITSPERSAMPLE, false);
                            if (e != null)
                                bitspix = ((Integer) e.getValue(0)).intValue();
                            int simpleperpix = 3;
                            e = getTagValue(SAMPLESPERPIXEL, false);
                            if (e != null)
                                simpleperpix = ((Integer) e.getValue(0))
                                        .intValue();
                            // 1. transfer image to int array
                            int[] image = new int[imgwidth * imglength];
                            for (int i = 0; i < image.length; i++) {
                                image[i] = ((data[i * 3] & 255) << 16)
                                        + ((data[i * 3 + 1] & 255) << 8)
                                        + (data[i * 3 + 2] & 255) + 0xFF000000;
                            }
                            MemoryImageSource mis = new MemoryImageSource(
                                    imgwidth, imglength, image, 0, imgwidth);
                            Image img = Toolkit.getDefaultToolkit()
                                    .createImage(mis);
                            image = null;
                            return new ImageIcon(img);
                        }
                    } catch (IOException x) {
                        if(Log.debugLevel >= Log.LEVEL_ERROR)
                            x.printStackTrace(System.err);
                    }
                }
            } finally {
                try {
                    is.close();
                } catch (Exception e) {
                }
            }
        }
        if (getAdvancedImage() != null) {
            try {
                // try advanced image API
                return getAdvancedImage().createThumbnailIcon(
                        getImageFile().getPath(), null);
            } catch (Throwable e) {
                if(Log.debugLevel >= Log.LEVEL_ERROR)
                    System.err.println("AdvImage:" + e);
            }
        }
        if(Log.debugLevel >= Log.LEVEL_INFO)
            System.out.println("Embedded thumbnail not found for " + getImageFile());
        return null;
    }

    /**
     * @return Exif Image Width
     */
    public int getResolutionX() {
        Entry e = getTagValue(EXIFIMAGEWIDTH, true);
        if (e != null)
            return ((Integer) e.getValue(0)).intValue();
        return -1;
    }

    /**
     * Sets Exif Image Width
     * @param xRes x Resolution
     */
    public void setResolutionX(int xRes) {
        Entry e = getTagValue(EXIFIMAGEWIDTH, true);
        if (e == null) {
            e = new Entry(LONG);
            setTagValue(EXIFIMAGEWIDTH, 0, e, true);
        }
        e.setValue(0, new Integer(xRes));
    }

    /**
     * @return Exif Image Height
     */
    public int getResolutionY() {
        Entry e = getTagValue(EXIFIMAGELENGTH, true);
        if (e != null)
            return ((Integer) e.getValue(0)).intValue();
        return -1;
    }

    /**
     * Sets Exif Image Height
     * @param yRes y Resolution
     */
    public void setResolutionY(int yRes) {
        Entry e = getTagValue(EXIFIMAGELENGTH, true);
        if (e == null) {
            e = new Entry(LONG);
            setTagValue(EXIFIMAGELENGTH, 0, e, true);
        }
        e.setValue(0, new Integer(yRes));
    }

    public int getMetering() {
        Entry e = getTagValue(METERINGMODE, true);
        if (e != null)
            return ((Integer) e.getValue(0)).intValue();
        return 0;
    }

    public String getMeteringAsString() {
        int m = getMetering();
        if (m >= 0 && m < METERING_MODES.length)
            return METERING_MODES[m];
        return "" + m;
    }

    public int getExpoProgram() {
        Entry e = getTagValue(EXPOSUREPROGRAM, true);
        if (e != null)
            return ((Integer) e.getValue(0)).intValue();
        return 0;
    }

    public String getExpoProgramAsString() {
        int ep = getExpoProgram();
        if (ep >= 0 && ep < EXPOSURE_PROGRAMS.length)
            return EXPOSURE_PROGRAMS[ep];
        return "" + ep;
    }

    public String getMake() {
        Entry e = getTagValue(MAKE, true);
        if (e != null)
            return e.toString();
        return NA;
    }

    public String getModel() {
        Entry e = getTagValue(MODEL, true);
        if (e != null)
            return e.toString();
        return NA;
    }

    public String getDataTimeOriginalString() {
        Entry e = getTagValue(DATETIMEORIGINAL, true);
        if (e != null) {
            String result = e.toString();
            if (result.indexOf("0000:00:00") < 0)
                return result;
        }
        return dateformat.format(new Date());
    }

    public float getFNumber() {
        Entry e = getTagValue(FNUMBER, true);
        if (e != null)
            return ((Rational) e.getValue(0)).floatValue();
        e = getTagValue(APERTUREVALUE, true);
        if (e != null)
            return apertureToFnumber(((Rational) e.getValue(0)).floatValue());
        return -1;
    }

    public Rational getShutter() {
        Entry e = getTagValue(EXPOSURETIME, true);
        if (e != null)
            return (Rational) e.getValue(0);
        e = getTagValue(SHUTTERSPEEDVALUE, true);
        try {
            return TV_TO_SEC[(int) ((Rational) e.getValue(0)).floatValue()];
        } catch (NullPointerException x) {
        } catch (ArrayIndexOutOfBoundsException x) {
        }
        return new Rational(0, 1);
    }

    public boolean isFlash() {
        Entry e = getTagValue(FLASH, true);
        if (e != null)
            return (((Integer) e.getValue(0)).intValue() & 1) == 1;
        return false;
    }

    // TODO: make the coefficients camera specific
    public float getFocalLength() {
        Entry e = (Entry) getTagValue(FOCALLENGTH, true);
        if (e != null)
            return Math.round((float) (38 * ((Rational) e.getValue(0))
                    .floatValue() / 5.8));
        return 0;
    }

    public String getQuality() {
        // TODO: check MAKE nad read from XML database
        Entry e = getTagValue(COMPRESSEDBITSPERPIXEL, true);
        if (e == null)
            return "Unknown";
        switch (((Rational) e.getValue(0)).intValue()) {
            case 1:
                return "BASIC";
            case 2:
                return "NORMAL";
            case 4:
                return "FINE";
        }
        return getTagValue(COMPRESSEDBITSPERPIXEL, true).toString();
    }

    public String getOrientation() {
        Entry e = getTagValue(ORIENTATION, true);
        int orientation = ((Integer) e.getValue(0)).intValue();
        if (orientation>0 && orientation<=Naming.OrientationNames.length)
            return Naming.OrientationNames[orientation-1];

        return "Unknown";
    }

    public String getReport() {
        StringBuffer report = new StringBuffer();
        Entry e = getTagValue(EXPOSURETIME, true);
        report.append("Shutter: ");
        if (e != null)
            report.append(e.toString());
        else {
            e = getTagValue(SHUTTERSPEEDVALUE, true);
            if (e != null) {
                report.append(e.toString());
            } else
                report.append(NA);
        }
        report.append(", Aperture: ");
        e = getTagValue(FNUMBER, true);
        if (e == null) {
            e = getTagValue(APERTUREVALUE, true);
            if (e != null)
                report.append(fnumberformat.format(((Rational) e.getValue(0))
                        .floatValue() * 0.4 + 1));
            else
                report.append(NA);
        } else
            report.append(fnumberformat.format(((Rational) e.getValue(0))
                    .floatValue()));
        report.append(", Flash: ");
        e = getTagValue(FLASH, true);
        if (e != null)
            report
                    .append((((Integer) e.getValue(0)).intValue() == 1) ? ImageResources.YES
                            : ImageResources.NO);
        else
            report.append(NA);
        return report.toString();
    }

    /**
     * Reads the imageInfo from the Input supplied in Constructor.
     */
    public void readInfo() {
        ifds = new IFD[2];
        offset -= data.length;
        intel = data[6] == 'I';
        motorola = data[6] == 'M';
        if (!(intel || motorola))
            return;
        version = s2n(8, 2);
        processAllIFDs();
        String msg = correctThumbnailTags(data, 0);
        if(msg != null)
            if(Log.debugLevel >= Log.LEVEL_WARNING)
                System.err.println("Warning: Exif Read: " + msg);
        data = null; // for gc
    }

    /**
     * Returns the new Orientation Tag after applying a transformation.
     * @param tag Current Orientation tag
     * @param op Transformation as defined in LLJTran
     * @return New Orientation tag that should be set after Transforming the
     * image
     */
    public static int transformOrientationTag(int tag, int op) {
        int newTag = 0;
        if (tag >= 1 && tag <= 8) {
            int positions, newPositions;

            // Get 4 2-bit numbers:
            // n0 n1
            // n3 n2
            // on 4 image corners corresponding to orientation tag. The numbers
            // are packed into a single 8-bit number
            positions = posForOrientationTags[tag];
            newPositions = positions;
            // Find out new positions after transformation
            switch (op) {
                case LLJTran.TRANSPOSE:
                    // n0,n1,n2,n3 => n0,n3,n2,n1
                    newPositions = positions & ((3 << 6) | (3 << 2))
                            | ((positions & 3) << 4) | (positions >> 4) & 3;
                    break;
                case LLJTran.TRANSVERSE:
                    // n0,n1,n2,n3 => n2,n1,n0,n3
                    newPositions = positions & ((3 << 4) | 3)
                            | ((positions & (3 << 2)) << 4)
                            | ((positions & (3 << 6)) >> 4);
                    break;
                case LLJTran.ROT_90:
                    // n0,n1,n2,n3 => n3,n0,n1,n2
                    newPositions = (positions >> 2) | ((positions & 3) << 6);
                    break;
                case LLJTran.ROT_270:
                    // n0,n1,n2,n3 => n1,n2,n3,n0
                    newPositions = ((positions << 2) | (positions >> 6)) & 255;
                    break;
                case LLJTran.ROT_180:
                    // n0,n1,n2,n3 => n2,n3,n0,n1
                    newPositions = ((positions & 15) << 4) | (positions >> 4);
                    break;
                case LLJTran.FLIP_H:
                    // n0,n1,n2,n3 => n1,n0,n3,n2
                    newPositions = ((positions & (3 << 4)) << 2)
                            | ((positions & (3 << 6)) >> 2)
                            | ((positions & 3) << 2)
                            | ((positions & (3 << 2)) >> 2);
                    break;
                case LLJTran.FLIP_V:
                    // n0,n1,n2,n3 => n3,n2,n1,n0
                    newPositions = ((positions & 3) << 6)
                            | ((positions & (3 << 2)) << 2)
                            | ((positions & (3 << 4)) >> 2) | (positions >> 6);
                    break;
                case LLJTran.NONE:
                default:
                    break;
            }

            newTag = 0;
            // Reverse lookup newTag from newPositions
            do
                ++newTag;
            while (posForOrientationTags[newTag] != newPositions);
        }

        return newTag;
    }

    // Utility method if required for debugging
    private void printIfds(IFD ifd, int level, PrintStream op) {
        if (ifd != null) {
            op.println("print Lvl = " + level);
            Map m = ifd.getEntries();
            for (Iterator e = m.keySet().iterator(); e.hasNext();) {
                Integer key = ((Integer) e.next());
                int keyVal = key.intValue();
                Entry ent = (Entry) m.get(key);
                op.print("Key = 0x" + Integer.toHexString(keyVal)
                        + " Type = " + ent.getType());
                Object vals[] = ent.getValues();
                if (vals != null)
                    for (int i = 0; i < vals.length; ++i)
                        op.print(" Val" + i + " = " + vals[i]);
                else
                    op.print("'" + ent.toString() + "'");
                op.println();
            }
            IFD subIfds[] = ifd.getIFDs();
            if (subIfds != null)
                for (int i = 0; i < subIfds.length; ++i)
                    printIfds(subIfds[i], level + 1, op);
        }
    }

    /**
     * Writes modified or not Exif to out. APP header and its length are not
     * included so any wrapper should do that calculation.<p>
     *
     * This method is mainly for use by LLJTran to regenerate the Appx marker
     * Data for the imageInfo.
     *
     * @param markerData The existing markerData. This is used by Exif to read
     * the existing Thumbnail.
     * @param out Output Stream to write out the new markerData
     * @param op The transformation option. This is used to switch the width and
     * height in imageInfo if op is a ROT_90 like transform and transform
     * the orientation tag and Thumbnail if opted for.
     * @param options OPT_XFORM_.. options of LLJTran. LLJTran passes its
     * options directly to this method. This uses the imageInfo related flags
     * {@link LLJTran#OPT_XFORM_THUMBNAIL} and
     * {@link LLJTran#OPT_XFORM_ORIENTATION} and makes the necessary
     * changes to imageInfo depending on the transform specified by <b>op</b>
     * before writing.
     * @param modifyImageInfo If true the changes made to imageInfo are
     * retained, otherwise the state is restored at the end of the call.
     * @param imageWidth Actual Image Width. If this and imageHeight are
     * positive then they are used for the width and height in imageInfo and no
     * switching of width and height is done for ROT_90 like transforms.
     * @param imageHeight Actaul Image Height
     * @param encoding Encoding to be used when for writing out Character
     * information like comments.
     */
    public void writeInfo(byte markerData[], OutputStream out, int op,
                          int options, boolean modifyImageInfo, int imageWidth,
                          int imageHeight, String encoding) throws IOException {
        // TODO: this implementation takes twice memory than needed
        // it should be rewritten using byte[] and then copying to stream
        // version returning just byte[] is also very useful
        if (ifds == null)
            throw new IllegalStateException("EXIF data not filled.");

        Entry orgEntries[] = null;
        Object orgVals[] = null;
        int numOrgEntries = 0;
        Entry orientationEntry = null;
        Object orientationVal = null;
        if ((options & LLJTran.OPT_XFORM_ORIENTATION) != 0
                && (orientationEntry = getTagValue(ORIENTATION, true)) != null) {
            Object val = orientationEntry.getValue(0);
            // Now Transform Orientation tag
            int orientation = ((Integer) val).intValue();
            int newOrientation = transformOrientationTag(orientation, op);
            if (orientation != newOrientation) {
                orientationVal = val;
                orientationEntry.setValue(0, new Integer(newOrientation));
            }
        }

        boolean dimensionModified = false;
        Entry resX = null, resY;
        Object xVal = null, yVal = null;
        resY = getTagValue(EXIFIMAGELENGTH, true);
        if (resY != null) {
            yVal = resY.getValue(0);
            resX = getTagValue(EXIFIMAGEWIDTH, true);
            if (resX != null) {
                xVal = resX.getValue(0);
                if (imageWidth > 0 && imageHeight > 0) {
                    Integer xVal1 = new Integer(imageWidth);
                    Integer yVal1 = new Integer(imageHeight);
                    dimensionModified = true;
                    resY.setValue(0, yVal1);
                    resX.setValue(0, xVal1);
                    if(modifyImageInfo)
                    {
                        xVal = xVal1;
                        yVal = yVal1;
                    }
                }
            }
        }

        if (!modifyImageInfo) {
            orgEntries = new Entry[16];
            orgVals = new Object[orgEntries.length];
        }

        switch (op) {
            case LLJTran.TRANSPOSE:
            case LLJTran.TRANSVERSE:
            case LLJTran.ROT_90:
            case LLJTran.ROT_270:
                if (!dimensionModified && resX != null && resY != null) {
                    dimensionModified = true;
                    resY.setValue(0, xVal);
                    resX.setValue(0, yVal);
                }
                Entry eResX,
                        eResY;
                Object eResXVal,
                        eResYVal;
                for (int i = 0; i < 2; i++) {
                    eResX = getTagValue(XRESOLUTION, i == 0 ? true : false);
                    eResY = getTagValue(YRESOLUTION, i == 0 ? true : false);
                    if (eResX != null && eResY != null) {
                        eResXVal = eResX.getValue(0);
                        eResYVal = eResY.getValue(0);
                        if (!modifyImageInfo) {
                            orgEntries[numOrgEntries] = eResX;
                            orgVals[numOrgEntries] = eResXVal;
                            numOrgEntries++;
                            orgEntries[numOrgEntries] = eResY;
                            orgVals[numOrgEntries] = eResYVal;
                            numOrgEntries++;
                        }
                        eResY.setValue(0, eResXVal);
                        eResX.setValue(0, eResYVal);
                    }
                }
                break;
            case LLJTran.ROT_180:
            case LLJTran.FLIP_H:
            case LLJTran.FLIP_V:
            case LLJTran.NONE:
            default:
        }

        if (!modifyImageInfo) {
            if (orientationVal != null) {
                orgEntries[numOrgEntries] = orientationEntry;
                orgVals[numOrgEntries] = orientationVal;
                numOrgEntries++;
            }

            Entry e = getTagValue(JPEGINTERCHANGEFORMAT, false);
            Object val;
            if (e != null) {
                val = e.getValue(0);
                orgEntries[numOrgEntries] = e;
                orgVals[numOrgEntries] = val;
                numOrgEntries++;
            }
            e = getTagValue(JPEGINTERCHANGEFORMATLENGTH, false);
            if (e != null) {
                val = e.getValue(0);
                orgEntries[numOrgEntries] = e;
                orgVals[numOrgEntries] = val;
                numOrgEntries++;
            }

            if (dimensionModified) {
                orgEntries[numOrgEntries] = resX;
                orgVals[numOrgEntries] = xVal;
                numOrgEntries++;
                orgEntries[numOrgEntries] = resY;
                orgVals[numOrgEntries] = yVal;
                numOrgEntries++;
            }
        }

        // TODO: write to out FIRST_IFD_OFF bytes
        out.write(EXIF_MARK);
        if (intel) {
            out.write('I');
            out.write('I');
        } else {
            out.write('M');
            out.write('M');
        }
        out.write(n2s(version, 2));

        int emptySlot = EXIF_MARK.length + 2;
        // write offset of IFD
        out.write(n2s(emptySlot, 4));
        String msg = correctThumbnailTags(markerData, 4);
        if(msg != null)
            if(Log.debugLevel >= Log.LEVEL_WARNING)
                System.err.println("Warning: Exif Write: " + msg);
        for (int k = 0; k < 2; k++) {
            //System.err.println("--->IFD "+k+" offeset "+emptySlot);
            boolean isLast = false;
            if (k == 1 || ifds[k + 1] == null)
                isLast = true;
            emptySlot = writeIfd(markerData, out, emptySlot, ifds[k], op,
                    options, isLast, encoding);
        }

        for (int i = 0; i < numOrgEntries; ++i) {
            Entry e = orgEntries[i];
            if (e != null)
                e.setValue(0, orgVals[i]);
        }
    }

    /**
     * Method to write the imageInfo with a new Thumbnail.
     *
     * This method changes the imageInfo for the new Thumbnail and writes out
     * the corresponding Appx header data (without jpeg markers) with the new
     * new Thumbnail.
     *
     * @param newThumbnailData New Thumbnail image data
     * @param startIndex Offset within newThumbnailData where the image starts
     * @param len Length of Thumbnail Image
     * @param thumbnailExt Extension of the Thumbnail Image from
     * {@link ImageResources#EXT_JPEG ImageResources}
     * which identifies the format of the Thumbnail image. Exif supports only
     * JPEG and BMP formats for Thumbnail.
     * @param newExifOp Output to write out the new Appx data
     * @return True if successful, false otherwise
     */
    public boolean setThumbnail(byte newThumbnailData[], int startIndex, int len,
                                String thumbnailExt, OutputStream newExifOp) throws IOException {
        Entry ent;
        boolean retVal = true;

        if (ifds[1] == null) {
            ifds[1] = new IFD(1);
            ent = new Entry(SHORT);
            ent.setValue(0, new Integer(2));
            ifds[1].addEntry(RESOLUTIONUNIT, ent);
            ent = new Entry(RATIONAL);
            ent.setValue(0, new Rational(180, 1));
            ifds[1].addEntry(XRESOLUTION, ent);
            ent = new Entry(RATIONAL);
            ent.setValue(0, new Rational(180, 1));
            ifds[1].addEntry(YRESOLUTION, ent);
        }

        String ext = thumbnailExt.toLowerCase();

        ent = getTagValue(COMPRESSION, false);
        if (ent == null) {
            ent = new Entry(SHORT);
            setTagValue(COMPRESSION, 0, ent, false);
        }

        boolean isJpegThumbnail = false;

        if (ImageResources.EXT_JPEG.equals(ext)
                || ImageResources.EXT_JPG.equals(ext)) {
            isJpegThumbnail = true;
            ent.setValue(0, new Integer(6));
            ifds[1].removeEntry(STRIPOFFSETS);
            ifds[1].removeEntry(STRIPBYTECOUNTS);
            ifds[1].removeEntry(PHOTOMETRICINTERPRETATION);

            // Adjust Offset and length tags for writeInfo
            // TODO: If writeInfo uses any other info other than thumbnail or
            // if it cannot work with zero adjusted thumbnail offset then
            // rewrite.
            ent = getTagValue(JPEGINTERCHANGEFORMAT, false);
            if (ent == null) {
                ent = new Entry(LONG);
                setTagValue(JPEGINTERCHANGEFORMAT, 0, ent, false);
            }
            ent.setValue(0, new Integer(startIndex - 4 - FIRST_IFD_OFF));

            ent = getTagValue(JPEGINTERCHANGEFORMATLENGTH, false);
            if (ent == null) {
                ent = new Entry(LONG);
                setTagValue(JPEGINTERCHANGEFORMATLENGTH, 0, ent, false);
            }
            ent.setValue(0, new Integer(len));
        } else {
            ent.setValue(0, new Integer(1));
            ifds[1].removeEntry(JPEGINTERCHANGEFORMAT);
            ifds[1].removeEntry(JPEGINTERCHANGEFORMATLENGTH);

            // Adjust Offset and length tags for writeInfo
            // TODO: If writeInfo uses any other info other than thumbnail or
            // if it cannot work with zero adjusted thumbnail offset then
            // rewrite.
            ent = getTagValue(STRIPOFFSETS, false);
            if (ent == null) {
                ent = new Entry(LONG);
                setTagValue(STRIPOFFSETS, 0, ent, false);
            }
            ent.setValue(0, new Integer(startIndex - 4 - FIRST_IFD_OFF));

            ent = getTagValue(STRIPBYTECOUNTS, false);
            if (ent == null) {
                ent = new Entry(LONG);
                setTagValue(STRIPBYTECOUNTS, 0, ent, false);
            }
            ent.setValue(0, new Integer(len));
        }

        writeInfo(newThumbnailData, newExifOp, LLJTran.NONE, 0, true);

        if(isJpegThumbnail)
        {
            if(getTagValue(JPEGINTERCHANGEFORMAT, false) == null)
                retVal = false;
        }
        else
        if(getTagValue(STRIPOFFSETS, false) == null)
            retVal = false;

        return retVal;
    }

    /**
     * Removes the Thumbnail Tags in the imageInfo. Thus the next time the Appx
     * is written using
     * {@link #writeInfo(byte[],OutputStream,int,int,boolean,int,int,String) writeInfo(..)}
     * it will be without a Thumbnail
     *
     * @return True if the Sub IFD containing Thumbnail is present
     */
    public boolean removeThumbnailTags() {
        IFD ifd = ifds[1];
        if (ifd != null) {
            ifds[1].removeEntry(JPEGINTERCHANGEFORMAT);
            ifds[1].removeEntry(JPEGINTERCHANGEFORMATLENGTH);
            ifds[1].removeEntry(STRIPOFFSETS);
            ifds[1].removeEntry(STRIPBYTECOUNTS);
            ifds[1].removeEntry(PHOTOMETRICINTERPRETATION);
        }

        return true;
    }

    // Removes Thumbnail tags if offset/length are not okay. Truncates
    // length if it goes outside marker. Returns null if okay or warning
    // message otherwise.
    private String correctThumbnailTags(byte markerData[], int leading)
    {
        String retVal = null;
        boolean thumbnailTagsPresent = false;
        boolean isJpegThumbnail = false;
        int offsetTagVal = 0;
        int offset = 0;
        Entry offsetEnt = getTagValue(JPEGINTERCHANGEFORMAT, false);
        if (offsetEnt == null)
            offsetEnt = getTagValue(STRIPOFFSETS, false);
        else
            isJpegThumbnail = true;
        if (offsetEnt != null)
        {
            thumbnailTagsPresent = true;
            offsetTagVal = ((Integer) offsetEnt.getValue(0)).intValue();
            offset = offsetTagVal + FIRST_IFD_OFF + leading;
        }

        int length = 0;
        Entry lengthEnt = getTagValue(JPEGINTERCHANGEFORMATLENGTH, false);
        if (lengthEnt == null)
            lengthEnt = getTagValue(STRIPBYTECOUNTS, false);
        else
            isJpegThumbnail = true;
        if (lengthEnt != null)
        {
            length = ((Integer) lengthEnt.getValue(0)).intValue();
            thumbnailTagsPresent = true;
        }
        int orgLen = length;

        if(thumbnailTagsPresent)
        {
            int lengthOvershoot = 0, skipCount = 0;
            StringBuffer warnBuf = new StringBuffer();
            if(markerData == null)
                retVal = "Removing Thumbnail: No Marker Supplied";
            else if(offset < 0 || offset > markerData.length)
                retVal = "Removing Thumbnail: Invalid Offset: " + offset;
            else if( (lengthOvershoot = offset + length - markerData.length) > 0)
            {
                length -= lengthOvershoot;
                warnBuf.append("; Thumbnail length ").append(orgLen)
                        .append(" is beyond Exif header. Reducing it to ")
                        .append(length);
            }

            if(retVal == null)
            {
                if(isJpegThumbnail)
                {
                    while (offset < markerData.length - 1 && length > 0 &&
                            !(markerData[offset] == M_PRX &&
                                    markerData[offset + 1] == M_SOI))
                    {
                        length--;
                        offset++;
                        skipCount++; // skip garbage in begining including padding FF
                    }

                    if(skipCount > 0)
                    {
                        offsetTagVal += skipCount;
                        warnBuf.append("; Skipped ").append(skipCount)
                                .append(" Garbage bytes at the beginning of Jpeg Thumbnail");
                    }
                }

                if(length <= MIN_JPEG_SIZE)
                {
                    warnBuf.append("; Removing Thumbnail: Invalid length: ")
                            .append(length);
                    retVal = warnBuf.substring(2);
                }
            }

            if(retVal != null)
                removeThumbnailTags();
            else if(lengthOvershoot > 0 || skipCount > 0)
            {
                retVal = warnBuf.substring(2);
                lengthEnt.setValue(0, new Integer(length));
                offsetEnt.setValue(0, new Integer(offsetTagVal));
            }
            warnBuf = null;
        }

        return retVal;
    }

    /** writes IFD from map and returns length
     */
    protected int writeIfd(byte markerData[], OutputStream out, int emptySlot,
                           IFD ifd, int op, int options, boolean isLast, String encoding)
            throws IOException {
        if (ifd == null) {
            if(Log.debugLevel >= Log.LEVEL_WARNING)
                System.err.println("Warning: Requested to write NULL IFD, nothing written.");
            return emptySlot;
        }
        ByteArrayOutputStream buf = new ByteArrayOutputStream(1 * 1024);
        int ne = (ifd.getEntries() == null ? 0 : ifd.getEntries().size())
                + (ifd.getIFDs() == null ? 0 : ifd.getIFDs().length);
        //System.err.println("ifd= "+Integer.toHexString(ifd.getTag())+" entries "+ne+" offset 0x"+Integer.toHexString(emptySlot));
        out.write(n2s(ne, 2)); // num entries
        emptySlot += ne * DIR_ENTRY_SIZE + 2 + 4; // num entries + next slot
        Iterator it = ifd.getEntries().entrySet().iterator();
        boolean foundJpegThumbnailTag = false;
        boolean foundBmpThumbnailTag = false;
        while (it.hasNext()) {
            Map.Entry me = (Map.Entry) it.next();
            int tag = ((Integer) me.getKey()).intValue();

            // Skip Thumbnail Tags to process at end. Processing at end
            // keeps Thumbnail tags next to Thumbnail data which programs
            // like jhead prefer
            if (tag == JPEGINTERCHANGEFORMAT) // skip it
            {
                foundJpegThumbnailTag = true;
                continue;
            }
            if (tag == JPEGINTERCHANGEFORMATLENGTH) // skip it
                continue;
            if (tag == STRIPOFFSETS) // skip it
            {
                foundBmpThumbnailTag = true;
                continue;
            }
            if (tag == STRIPBYTECOUNTS) // skip it
                continue;

            Entry e = (Entry) me.getValue();
            if (e == null)
                continue;
            // TODO: consider write(e.toByteArray(intel)
            out.write(n2s(tag, 2));
            int type;
            out.write(n2s(type = e.getType(), 2));
            //System.err.println("write type "+Integer.toString(type,16)+" tag "+Integer.toString(tag, 16)+
            //                 " tag vals "+rogatkin.DataConv.arrayToString(e.getValues(), ':'));
            if (type == ASCII) {
                byte[] str = e.toString().getBytes(encoding);
                out.write(n2s(str.length + 1, 4));
                if (str.length + 1 > 4) {
                    out.write(n2s(emptySlot, 4));
                    buf.write(str); // buf used
                    buf.write(0);
                    emptySlot += str.length + 1;
                } else { // write data
                    out.write(str);
                    if (str.length < 4) // write padding
                        for (int i = 0; i < 4 - str.length; i++)
                            // shouldn't be a stopper
                            out.write(0);
                }
            } else {
                Object[] vs = e.getValues(); // can vs be null ? or have length 0
                out.write(n2s(vs.length, 4));
                int tlen = TYPELENGTH[type - 1];
                if (vs.length * tlen > 4) {
                    out.write(n2s(emptySlot, 4));
                    boolean signed = (SBYTE == 6 || type >= SSHORT);
                    boolean rational = type % RATIONAL == 0;
                    for (int i = 0; i < vs.length; i++) {
                        if (rational) {
                            buf.write(n2s(((Rational) vs[i]).getNum(), 4));
                            buf.write(n2s(((Rational) vs[i]).getDen(), 4));
                            emptySlot += 8;
                        } else {
                            buf.write(n2s(((Integer) vs[i]).intValue(), tlen));
                            emptySlot += tlen;
                        }
                    }
                } else {
                    for (int i = 0; i < vs.length; i++)
                        out.write(n2s(((Integer) vs[i]).intValue(), tlen));
                    if (vs.length * tlen < 4)
                        for (int i = 0; i < 4 - vs.length * tlen; i++)
                            // shouldn't be a stopper
                            out.write(0);
                }
            }
        }

        // Write Thumbmnail offset and length if found
        if (foundJpegThumbnailTag) {
            int length = getThumbnailLength();
            int jpeg_offset = getThumbnailOffset() + 4;
            // Not doing any validity checks. Validation and correction should
            // have been done by calling correctThumbnailTags
            try {
                int l = buf.size();
                boolean copyThumbnail = true;
                if ((options & LLJTran.OPT_XFORM_THUMBNAIL) != 0
                        && op != LLJTran.NONE && op != LLJTran.CROP) {
                    LLJTran ljt = null;
                    try
                    {
                        ByteArrayInputStream tis = new ByteArrayInputStream(
                                markerData, jpeg_offset, length);
                        ljt = new LLJTran(tis);
                        ljt.read(LLJTran.READ_ALL, false);
                        ljt.transform(op, 0);
                        copyThumbnail = false;
                        tis = null;
                    } catch (Throwable e)
                    {
                        if(Log.debugLevel >= Log.LEVEL_WARNING)
                        {
                            System.err.println("Warning: Unable to Transform Thumbnail, will write it unchanged: " + e.getMessage());
                            e.printStackTrace(System.err);
                        }
                    }

                    // Hope there won't be an exception just now
                    if(!copyThumbnail)
                        ljt.save(buf, 0);
                }

                if(copyThumbnail)
                    buf.write(markerData, jpeg_offset, length);
                l = buf.size() - l;
                Entry ent = getTagValue(JPEGINTERCHANGEFORMATLENGTH,
                        false);
                if (ent != null)
                    ent.setValue(0, new Integer(l));
                out.write(n2s(JPEGINTERCHANGEFORMATLENGTH, 2));
                out.write(n2s(LONG, 2));
                out.write(n2s(1, 4));
                out.write(n2s(l, 4));
                ent = getTagValue(JPEGINTERCHANGEFORMAT, false);
                ent.setValue(0, new Integer(emptySlot));
                out.write(n2s(JPEGINTERCHANGEFORMAT, 2));
                out.write(n2s(ent.getType(), 2));
                out.write(n2s(1, 4));
                out.write(n2s(emptySlot, 4));
                emptySlot += l;
            } catch (Throwable t) {
                if(Log.debugLevel >= Log.LEVEL_ERROR)
                    t.printStackTrace(System.err);
            }
        } else if (foundBmpThumbnailTag) {
            int length = getThumbnailLength();
            int offset = getThumbnailOffset() + 4;
            // Not doing any validity checks. Validation and correction should
            // have been done by calling correctThumbnailTags
            try {
                int l = buf.size();
                buf.write(markerData, offset, length);
                l = buf.size() - l;

                Entry ent = getTagValue(STRIPBYTECOUNTS, false);
                if (ent != null)
                    ent.setValue(0, new Integer(l));
                out.write(n2s(STRIPBYTECOUNTS, 2));
                out.write(n2s(LONG, 2));
                out.write(n2s(1, 4));
                out.write(n2s(l, 4));
                ent = getTagValue(STRIPOFFSETS, false);
                ent.setValue(0, new Integer(emptySlot));
                out.write(n2s(STRIPOFFSETS, 2));
                out.write(n2s(ent.getType(), 2));
                out.write(n2s(1, 4));
                out.write(n2s(emptySlot, 4));
                emptySlot += l;
            } catch (Throwable t) {
                if(Log.debugLevel >= Log.LEVEL_ERROR)
                    t.printStackTrace(System.err);
            }
        }

        // write IFDs
        IFD[] ifds = ifd.getIFDs();
        for (int k = 0; ifds != null && k < ifds.length;) {
            IFD ifd1 = ifds[k];
            out.write(n2s(ifd1.getTag(), 2));
            out.write(n2s(ifd1.getType(), 2));
            out.write(n2s(1, 4));
            out.write(n2s(emptySlot, 4));
            k++;
            // Passing the isLast parameter for below call assumes that there
            // are no null entries in SubIfds
            emptySlot = writeIfd(markerData, buf, emptySlot, ifd1, op, options,
                    (k==ifds.length), encoding);
        }
        // next IFD
        out.write(n2s(isLast ? 0 : emptySlot, 4));
        // write data
        buf.writeTo(out);

        return emptySlot;
    }

    protected int firstIFD() {
        //System.err.println("FIFD "+(s2n(FIRST_IFD_OFF+4, 4)+FIRST_IFD_OFF));
        return s2n(FIRST_IFD_OFF + 4, 4) + FIRST_IFD_OFF;
    }

    protected int nextIFD(int ifd) {
        int entries = s2n(ifd, 2);
        return s2n(ifd + 2 + DIR_ENTRY_SIZE * entries, 4) + FIRST_IFD_OFF;
    }

    protected void processAllIFDs() {
        int iifd = 0;
        for (int i = firstIFD(); i > FIRST_IFD_OFF && iifd < 2; i = nextIFD(i)) {
            ifds[iifd] = new IFD(iifd);
            storeIFD(i, ifds[iifd]);
            iifd++;
        }
    }

    protected void storeIFD(int ifdoffset, IFD ifd) {
        int entries = s2n(ifdoffset, 2);
        //System.err.println("Store off "+ifdoffset+" tag "+Integer.toHexString(ifd.getTag())+" entries "+entries);
        for (int i = 0; i < entries; i++) {
            int entry = ifdoffset + 2 + DIR_ENTRY_SIZE * i;
            int tag = s2n(entry, 2);
            int type = s2n(entry + 2, 2);
            if (type < 1 || type > 10)
                continue; // not handled
            int typelen = TYPELENGTH[type - 1];
            int count = s2n(entry + 4, 4);
            int offset = entry + 8;
            if (count * typelen > 4)
                offset = s2n(offset, 4) + FIRST_IFD_OFF;
            //System.err.println("tag "+Integer.toHexString(tag)+" type "+type+" len "+ count +" off "+offset);
            if (type == ASCII) {
                // Special case: zero-terminated ASCII string
                try {
                    ifd.addEntry(tag, new Entry(type, new String(data, offset,
                            count - 1, "Default")));
                } catch (UnsupportedEncodingException e) {
                    if(Log.debugLevel >= Log.LEVEL_ERROR)
                        System.err.println("storeIFD: getString() " + e);
                }
            } else {
                Object[] values = new Object[count];
                boolean signed = (SBYTE == 6 || type >= SSHORT);
                for (int j = 0; j < count; j++) {
                    if (type % RATIONAL != 0)
                        // Not a fraction
                        values[j] = new Integer(s2n(offset, typelen, signed));
                    else
                        // The type is either 5 or 10
                        values[j] = new Rational(s2n(offset, 4, signed), s2n(
                                offset + 4, 4, signed));
                    offset += typelen;
                    // Recent Fujifilm and Toshiba cameras have a little subdirectory
                    // here, pointed to by tag 0xA005. Apparently, it's the
                    // "Interoperability IFD", defined in Exif 2.1.
                    if ((tag == EXIFOFFSET || tag == INTEROPERABILITYOFFSET || tag == GPSINFO /*|| tag == MAKERNOTE*/)
                            && j == 0 && ((Integer) values[0]).intValue() > 0) {
                        IFD iifd;
                        storeIFD(((Integer) values[0]).intValue()
                                + FIRST_IFD_OFF, iifd = new IFD(tag, type));
                        ifd.addIFD(iifd);
                    } else
                        ifd.addEntry(tag, new Entry(type, values));
                }
            }
        }
    }

    /**
     * return IFDs
     */
    public IFD[] getIFDs() {
        return ifds;
    }

    // Assume 4 corners of original image have a number 0-3 marked
    // clockwise as below:
    // 0    1
    // 3    2
    // The below lookup array gives the numbers on each corner of the oriented
    // image corresponding to each orientation tag represented as a 8-bit
    // number, 2-bits per number on each corner of the oriented image
    private static final int posForOrientationTags[] = { -1,
            (1 << 4) + (2 << 2) + 3, // tag 1 => NONE: 0,1,2,3
            (1 << 6) + (3 << 2) + 2, // tag 2 => FLIP_H: 1,0,3,2
            (2 << 6) + (3 << 4) + 1, // tag 3 => ROT_180: 2,3,0,1
            (3 << 6) + (2 << 4) + (1 << 2), // tag 4 => FLIP_V: 3,2,1,0
            (3 << 4) + (2 << 2) + 1, // tag 5 => TRANSPOSE: 0,3,2,1
            (1 << 6) + (2 << 4) + (3 << 2), // tag 6 => ROT_270: 1,2,3,0
            (2 << 6) + (1 << 4) + 3, // tag 7 => TRANSVERSE: 2,1,0,3
            (3 << 6) + (1 << 2) + 2 // tag 8 => ROT_90: 3,0,1,2
    };

    /**
     * A lookup array which can be used to get the LLJTran transformation
     * operation required to correct the orientation for a given Exif
     * Orientation Tag
     */
    public static final int opToCorrectOrientation[] = { -1, LLJTran.NONE,
            LLJTran.FLIP_H, LLJTran.ROT_180, LLJTran.FLIP_V, LLJTran.TRANSPOSE,
            LLJTran.ROT_90, LLJTran.TRANSVERSE, LLJTran.ROT_270 };

    protected int currentimage;

    protected int version;

    protected IFD[] ifds;
}
